Mybatis之Possible unexpected auto-mapping

  最近在做项目改造,将项目改造成SpringBoot应用。主要工作规整旧的配置,调整部分代码结构,对项目的代码未做变动,完成后提交给QA做回归验证。
  就在上周,测试同学将我唤了过去,说有页面请求接口报500。看到现象顿感疑惑,首先我并未修改之前的代码,查看线上master分支是运行正常的,是否是测试环境有脏数据?或者登陆异常?
  查看测试环境的日志,代码有一处抛了空指针,为了简化描述,还原当时的代码场景如下:

1
2
3
4
105 for (Account account : accounts) {
106 User user = userService.findById(account.getUserId());
107 String userId = user.getName() + "(" + account.getUserId() + ")";
108 }

  日志分析空指针是在第107行的 user.getName() 抛出的。一眼看去,的确未对user是否为空作判断,遂执行查询数据库的SQL,看是否有account和user数据不一致的情况,结果user均可查到!让我们看下AccountMapper.xml里的查询:

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="BaseResultMap" type="com.xxx.User">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="user_id" jdbcType="VARCHAR" property="userId" />
</resultMap>
<select id="getAccountDetail" resultMap="BaseResultMap">
SELECT
id,
user_id as userId
from account
</select>

  这样看,userId拿出来肯定是空的呀,我们使用的对象里属性是userId,为什么线上master分支还能正确执行?(当时怀疑合代码的问题,后查看master分支代码是一致的)。
  为什么相同的代码跑出了不同的结果,还和我预想的结果刚好相反。(猜想是master运行应该也会报错才对)。不对,还是可能存在不同的代码,就是我引入的jar包。在原来的Web工程里,引入的mybais是:

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.2</version>
<dependency>

当前springboot应用引入的依赖里,使用的mybatis是3.4.5。

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
<dependency>

  难道是这两个版本之间的改动?可能别人也遇到过这样的问题。于是到GitHub的mybatis项目上看3.4.3到3.4.5的发布日志,发现了蛛丝马迹:

  我们由此知道在3.4.2出现的现象是个bug,3.4.3版本修复了这个问题。问题已经找到。解决办法呢?是修改原有代码还是将错就错?由于原版本一直良好运行,以及提升版本也可能出现其他的问题,于是选择继续沿用3.4.2版本。
  坑就如同上述,但是导致这一问题的原因是什么,PR #895 是如何修复这个bug的呢?
  我们需要对源码作进一步分析。对这个bug的修复主要涉及两个文件——DefaultResultSetHandler.java和ResultMap.java。DefaultResultSetHandler处理SQL查询结果集,生成结果列表。通过debug看到结果映射调用堆栈是(代码版本mybatis 3.4.2):

  • loadMappedAndUnmappedColumnNames
  • getUnmappedColumnNames
  • createAutomaticMappings
  • applyAutomaticMappings
  • getRowValue
  • handleRowValuesForSimpleResultMap
  • handleRowValues

查看代码片段1,select 出的 userId 因为没有在resultMap里找到column映射,所以被放在unmappedColumnNames里返回。

查看代码片段2,尝试将unmappedColumnNames放入automapping。

查看代码片段3,得到userId的value.

查看代码片段4,代码里先automapping,再使用propertymapping,所以查出的结果userId值获取到了。

  当前的判断逻辑是在unmappedColumnNames中的属性,如果对象里有该属性且有set方法会被自动映射(mybatis自动映射是默认打开的)。实际应该在resultmap里未被映射的情况下才使用自动映射,即存在映射如下,默认的userId -> userId不应该生效。

1
<result column="user_id" jdbcType="VARCHAR" property="userId" />

  所以在修复的版本(mybatis 3.4.3)中在增加resultmap的property记录,并且增加if判断条件。即unmappedColumnNames中的userId在判断时包含在mappedProperties里,不会被自动映射,会遵从xml里的resultmap。

  至此问题得到解决,本文主要围绕问题#895 分析,感谢阅读。